Skip to content

Support custom timeouts for txn confirmation#3

Merged
gregnazario merged 7 commits intodecibeltrade:mainfrom
dizpers:feat/custom-timeout-support
Apr 6, 2026
Merged

Support custom timeouts for txn confirmation#3
gregnazario merged 7 commits intodecibeltrade:mainfrom
dizpers:feat/custom-timeout-support

Conversation

@dizpers
Copy link
Copy Markdown
Contributor

@dizpers dizpers commented Apr 1, 2026

Summary

This PR is based on the changes made in #2. The whole idea is to add configurable timeout for both transaction submission and confirmation.

Changes

  • Separate configurable timeout for txn submission and txn confirmation
  • A way to specify timeout in order placement (and other functions)
  • Default values for txn submission timeout and txn confirmation timeouts
  • Custom exception for txn submission failure and txn confirmation failure

Context

Some of the operations (e.g. orders cancellation) is taking more than default 30 seconds to confirm the transaction. So the custom software (e.g. Hummingbot connector) needs a way to explicitly set the timeout for those operations, set a bigger value.

E.g. order cancellation on testnet is taking up to 90 seconds, while default timeout was just 30 seconds. So it's possible that we (1) send a txn and it succeeds in 90 seconds (2) but timeout is 30 seconds (3) a trading bot see timeout error and retry cancellation for an order which is already cancelled.

Custom exceptions are needed for the client software can understand if a txn was submitted or not. To perform re-try logic.

Usage Example

# Async
await dex.place_order(
    market_name="BTC-USD",
    price=50000,
    size=1,
    is_buy=True,
    time_in_force=TimeInForce.GTC,
    is_reduce_only=False,
    txn_submit_timeout=30.0,    # HTTP submit timeout (default 10s)
    txn_confirm_timeout=90.0,   # Confirmation poll timeout (default 30s)
)

# Sync
dex_sync.place_order(
    ...,
    txn_submit_timeout=30.0,
    txn_confirm_timeout=90.0,
)

Testing

Tested on testnet and mainnet

@dizpers dizpers force-pushed the feat/custom-timeout-support branch 2 times, most recently from 6f016b9 to 02cf0b0 Compare April 1, 2026 16:43
@dizpers dizpers marked this pull request as draft April 3, 2026 12:20
@dizpers dizpers marked this pull request as ready for review April 5, 2026 08:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds configurable timeouts to the SDK’s transaction lifecycle so callers can independently tune HTTP submission timeouts vs on-chain confirmation polling timeouts, and introduces typed exceptions to distinguish “not submitted” vs “submitted but not confirmed/failed” cases.

Changes:

  • Add txn_submit_timeout and txn_confirm_timeout parameters across write APIs and the underlying _send_tx/polling flow.
  • Introduce TxnSubmitError and TxnConfirmError, and export them from the package.
  • Add default timeout constants and minor ABI tooling/help text tweaks.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/decibel/write/init.py Plumbs new timeout parameters through most write operations (async + sync).
src/decibel/_base.py Implements default submit/confirm timeouts; maps submission/confirmation failures to custom exceptions.
src/decibel/_fee_pay.py Adds timeout support when submitting via gas station / legacy fee payer endpoints.
src/decibel/_exceptions.py Defines new custom exception types for submit vs confirm failures.
src/decibel/_constants.py Adds DEFAULT_TXN_SUBMIT_TIMEOUT and DEFAULT_TXN_CONFIRM_TIMEOUT.
src/decibel/init.py Exports the new exception classes.
src/decibel/abi/generate.py Fixes CLI help text for supported networks.
src/decibel/abi/_registry.py Minor control-flow tweak (if -> elif) for chain selection.
.gitignore Removes explanatory comments for the .idea/ ignore rule.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"Authorization": f"Bearer {config.gas_station_api_key}",
}

async def _do_submit(c: httpx.AsyncClient) -> httpx.Response:
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing timeout=txn_submit_timeout when txn_submit_timeout is None will disable httpx timeouts entirely (httpx treats None as "no timeout"), which changes the previous default behavior (using client defaults). Consider only passing the timeout argument when a value is provided, or defaulting txn_submit_timeout to DEFAULT_TXN_SUBMIT_TIMEOUT inside this function to avoid potential indefinite hangs.

Suggested change
async def _do_submit(c: httpx.AsyncClient) -> httpx.Response:
async def _do_submit(c: httpx.AsyncClient) -> httpx.Response:
if txn_submit_timeout is None:
return await c.post(url, json=body, headers=headers)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can clean this up in a followup

"Authorization": f"Bearer {config.gas_station_api_key}",
}

def _do_submit(c: httpx.Client) -> httpx.Response:
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the async path: passing timeout=txn_submit_timeout when txn_submit_timeout is None disables httpx timeouts and changes default behavior. Prefer omitting the timeout kwarg when None, or apply a default (e.g., DEFAULT_TXN_SUBMIT_TIMEOUT) before calling client.post.

Suggested change
def _do_submit(c: httpx.Client) -> httpx.Response:
def _do_submit(c: httpx.Client) -> httpx.Response:
if txn_submit_timeout is None:
return c.post(url, json=body, headers=headers)

Copilot uses AI. Check for mistakes.
Comment on lines 236 to +242
if client is not None:
response = await client.post(url, json=body, headers=headers)
response = await client.post(url, json=body, headers=headers, timeout=txn_submit_timeout)
else:
async with httpx.AsyncClient() as temp_client:
response = await temp_client.post(url, json=body, headers=headers)
response = await temp_client.post(
url, json=body, headers=headers, timeout=txn_submit_timeout
)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this legacy fee payer submit call, timeout=txn_submit_timeout will disable timeouts when txn_submit_timeout is None. That can cause the request to hang indefinitely and is a behavioral change vs the previous httpx defaults. Consider only passing timeout when non-None, or setting a default timeout value at the start of the function.

Copilot uses AI. Check for mistakes.
Comment on lines 284 to +288
if client is not None:
response = client.post(url, json=body, headers=headers)
response = client.post(url, json=body, headers=headers, timeout=txn_submit_timeout)
else:
with httpx.Client() as temp_client:
response = temp_client.post(url, json=body, headers=headers)
response = temp_client.post(url, json=body, headers=headers, timeout=txn_submit_timeout)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue in the sync legacy fee payer path: timeout=None disables httpx timeouts. To preserve previous behavior and avoid indefinite hangs, omit the timeout kwarg when txn_submit_timeout is None or default it to a sensible value.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
except httpx.HTTPStatusError as e:
raise TxnSubmitError(
f"Failed to submit transaction: HTTP {e.response.status_code}",
original_exception=e,
) from e
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

httpx.HTTPStatusError is unlikely to be raised here because none of the submit helpers call response.raise_for_status(); non-2xx responses are converted into ValueError in _submit_direct / _fee_pay. This except block is effectively dead code and can mislead readers; either remove it or switch submission helpers to use response.raise_for_status() consistently.

Suggested change
except httpx.HTTPStatusError as e:
raise TxnSubmitError(
f"Failed to submit transaction: HTTP {e.response.status_code}",
original_exception=e,
) from e

Copilot uses AI. Check for mistakes.
Comment on lines +606 to +610
except httpx.HTTPStatusError as e:
raise TxnSubmitError(
f"Failed to submit transaction: HTTP {e.response.status_code}",
original_exception=e,
) from e
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the async version: catching httpx.HTTPStatusError here appears unreachable without response.raise_for_status(). Consider removing this handler or changing the submission code to raise HTTPStatusError intentionally for non-success responses.

Suggested change
except httpx.HTTPStatusError as e:
raise TxnSubmitError(
f"Failed to submit transaction: HTTP {e.response.status_code}",
original_exception=e,
) from e

Copilot uses AI. Check for mistakes.
Comment on lines +255 to +257
return await self._wait_for_transaction(
pending_tx.hash, txn_confirm_timeout=txn_confirm_timeout
)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New public behavior is introduced here (separate submit vs confirm timeouts and new TxnSubmitError/TxnConfirmError types). The repo uses pytest, but there don't appear to be tests covering these timeout paths and exception mapping. Adding unit tests (e.g., mocking httpx to force timeouts, non-success submission responses, and confirmation polling timeouts/VM failures) would help prevent regressions and document intended retry semantics.

Suggested change
return await self._wait_for_transaction(
pending_tx.hash, txn_confirm_timeout=txn_confirm_timeout
)
try:
return await self._wait_for_transaction(
pending_tx.hash, txn_confirm_timeout=txn_confirm_timeout
)
except TxnConfirmError:
raise
except httpx.ConnectTimeout as e:
raise TxnConfirmError(
f"Failed to confirm transaction {pending_tx.hash}: connection timeout to {self._config.fullnode_url}",
original_exception=e,
) from e
except httpx.ConnectError as e:
raise TxnConfirmError(
f"Failed to confirm transaction {pending_tx.hash}: connection error - {e}",
original_exception=e,
) from e
except httpx.HTTPStatusError as e:
raise TxnConfirmError(
f"Failed to confirm transaction {pending_tx.hash}: HTTP {e.response.status_code}",
original_exception=e,
) from e
except Exception as e:
raise TxnConfirmError(
f"Failed to confirm transaction {pending_tx.hash}: {e}",
original_exception=e,
) from e

Copilot uses AI. Check for mistakes.
Comment on lines 364 to +380
async with httpx.AsyncClient() as client:
while True:
response = await client.get(url, headers=headers)

if response.is_success:
data = cast("dict[str, Any]", response.json())
tx_type = data.get("type")
if tx_type == "pending_transaction":
pass
elif data.get("success") is True:
return data
elif data.get("success") is False:
vm_status = data.get("vm_status", "Unknown error")
raise ValueError(f"Transaction failed: {vm_status}")
raise TxnConfirmError(tx_hash, f"failed: {vm_status}")

if time.time() - start_time > timeout_secs:
raise TimeoutError(
f"Transaction {tx_hash} did not complete within {timeout_secs}s"
)
if time.time() - start_time > txn_confirm_timeout:
raise TxnConfirmError(tx_hash, f"did not confirm within {txn_confirm_timeout}s")
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_wait_for_transaction can still raise raw httpx exceptions (e.g., ReadTimeout/ConnectError) from client.get(), which makes it harder for callers to rely on TxnConfirmError as the "submitted but not confirmed" signal. Consider catching httpx.RequestError (and possibly non-success HTTP responses) inside the poll loop and re-raising TxnConfirmError so retry logic can treat all post-submission failures consistently.

Copilot uses AI. Check for mistakes.
Comment on lines 731 to 746
def poll_loop(client: httpx.Client) -> dict[str, Any]:
while True:
response = client.get(url, headers=headers)
if response.is_success:
data = cast("dict[str, Any]", response.json())
tx_type = data.get("type")
if tx_type == "pending_transaction":
pass
elif data.get("success") is True:
return data
elif data.get("success") is False:
vm_status = data.get("vm_status", "Unknown error")
raise ValueError(f"Transaction failed: {vm_status}")
if time.time() - start_time > timeout_secs:
raise TimeoutError(
f"Transaction {tx_hash} did not complete within {timeout_secs}s"
)
raise TxnConfirmError(tx_hash, f"failed: {vm_status}")
if time.time() - start_time > txn_confirm_timeout:
raise TxnConfirmError(tx_hash, f"did not confirm within {txn_confirm_timeout}s")
time.sleep(poll_interval_secs)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the async poller: client.get() in this confirmation loop can raise httpx exceptions that will escape as non-TxnConfirmError types, even though the transaction has already been submitted. Wrapping httpx.RequestError (and optionally non-success HTTP responses) as TxnConfirmError would make the retry semantics more consistent for SDK callers.

Copilot uses AI. Check for mistakes.
"Authorization": f"Bearer {config.gas_station_api_key}",
}

async def _do_submit(c: httpx.AsyncClient) -> httpx.Response:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can clean this up in a followup

@gregnazario gregnazario merged commit 8004d3b into decibeltrade:main Apr 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants